﻿//+------------------------------------------------------------------------------------------------------------------------------------------------------
//
//Description: Parameterization Parser.
//Author: Aravind Ramarathinam (aravr@microsoft.com)
//Copyright © Microsoft Corporation
//Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
//You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
//
//THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, 
//INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. 
//See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
// 
//--------------------------------------------------------------------------------------------------------------------------------------------------------


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;
using VmroleAuthor.ViewModels;

namespace VmroleAuthor.Models
{


    public enum ParameterOptions
    {
        CannotBeParameterizedBySpecification,
        ParameterizationSelected,
        ParameterizationNotSelected
    }

    public enum ExpectedType
    {
        Integer,
        NullableInteger,
        Bool,
        NullableBool,
        Credential,
        StringOrSecureString,
        Enum
    }



    public class ParameterizationHelper
    {
        public static string ParameterSyntax = "Param.";
        public static readonly int ParameterSyntaxLength = ParameterizationHelper.ParameterSyntax.Length;
        private const char ParamDelimiterBegin = '[';
        private const char ParamDelimiterEnd = ']';
        private const string ParameterNamePattern = "^[a-zA-Z][a-zA-Z0-9]*$";
        private static readonly Regex ParameterRegEx = new Regex(ParameterizationHelper.ParameterNamePattern);
        //private const string SensitiveParameterValue = "__**__";

        private const string ParameterSyntaxStringBegin = "[Param.";

        public static bool IsNullable(ExpectedType expectedType)
        {
            return expectedType == ExpectedType.NullableBool || expectedType == ExpectedType.NullableInteger;
        }

        public static string GenerateParameterString(string paramName)
        {
            string generated = ParameterSyntaxStringBegin + paramName + ParamDelimiterEnd;
            return generated;
        }

        public static bool IsFullyParameterized(string inputString)
        {
            return !string.IsNullOrEmpty(inputString) && inputString.StartsWith(ParameterSyntaxStringBegin, StringComparison.Ordinal) && inputString.EndsWith("]", StringComparison.Ordinal);
        }

        public static bool IsNotParameterized(string inputString)
        {
            List<string> paramNames;
            string error = ValidateAndGetParameterNames(inputString, out paramNames);
            return string.IsNullOrEmpty(error) && paramNames.Count == 0;
        }

        //public static bool IsPartiallyParameterized(string inputString)
        //{
        //    return !string.IsNullOrEmpty(inputString) && !IsFullyParameterized(inputString) && GetParameterNames(inputString).Count > 0;
        //}

        public static string ValidateAndGetParameterNames(string inputString, out List<string> paramNames)
        {
            List<ParsedParameterDetails> parsedParams;
            List<int> escapedDelimLocations;
            string error = ParseForParametersAndValidateSyntax(inputString, null, true, false, out parsedParams, out escapedDelimLocations);
            paramNames = parsedParams.Select(parsedParam => parsedParam.ParameterName).ToList();
            return error;
        }

        public const int MaxNonParameterizablePropertyValueLength = 100;

        public static string ValidateCannotBeParameterizedBySpecification(string stringToValidate, bool allowSpaces = false, bool validateLength = true)
        {
            string error = null;
            if (string.IsNullOrEmpty(stringToValidate))
            {
                error = "Cannot be empty";
                return error;
            }
            if (validateLength && stringToValidate.Length > MaxNonParameterizablePropertyValueLength)
            {
                error = "Cannot be longer than " + MaxNonParameterizablePropertyValueLength;
                return error;
            }
            string regex = allowSpaces ? "^[a-zA-Z0-9_-]+[ a-zA-Z0-9_-]*$" : "^[a-zA-Z0-9_-]+$";

            if (!Regex.IsMatch(stringToValidate, regex))
            {
                error = "Cannot be parameterized. Invalid characters. Does not match the expected regular expression: " + regex;
            }
            return error;
        }

        public static string ValidatePropertyParameterized(string inputString, ParametersViewModel parametersViewModel)
        {
            List<ParsedParameterDetails> parsedParams;
            List<int> escapedDelimLocations;
            string err = ParseForParametersAndValidateSyntax(inputString, null, true, false, out parsedParams, out escapedDelimLocations);
            if (string.IsNullOrEmpty(err))
            {
                StringBuilder sb = new StringBuilder();
                if (parsedParams.Count == 0)
                {
                    sb.Append("No parameter found. Syntax invalid?");
                }
                else
                {
                    if (parametersViewModel != null)
                    {
                        foreach (ParsedParameterDetails parsedParameterDetails in parsedParams)
                        {
                            if (!parametersViewModel.ContainsParameter(parsedParameterDetails.ParameterName))
                            {
                                sb.Append(" Parameter not declared: " + parsedParameterDetails.ParameterName);
                            }
                        }
                    }

                }
                err = sb.ToString();

            }
            return err;
        }

        public static string ValidatePropertyNotParameterized(string inputString, bool isNotNullable)
        {
            return ValidateInputNotParameterized(inputString, ParameterOptions.ParameterizationNotSelected, isNotNullable);
        }



        public class ParsedParameterDetails
        {
            public string ParameterName;
            public string ParameterValue;
            public int ParameterBeginIndex;
            public int ParameterLength;
        }


        /// <summary>
        /// Validates that the input string is parameterized correctly and Parse it for parameter details
        /// </summary>
        /// <param name="inputString"> input string </param>
        /// <param name="paramValues">used only during deployment. pass null if not available.</param>
        /// <param name="getParsedParamDetails">if true, parsedParams is constructed, else parsedParams is null</param>
        /// <param name="getEscapedDelimiterOptions">if true, escapedDelimiterLocations is constructed, else escapedDelimiterLocations is null</param>
        /// <param name="parsedParams">list of parameters in the input string. null if getParsedParamDetails is false</param>
        /// <param name="escapedDelimiterLocations"> null if getEscapedDelimiterOptions is false</param>
        /// <returns>First error message to be displayed. Null if there is no error</returns>
        public static string ParseForParametersAndValidateSyntax(string inputString, Dictionary<string, string> paramValues, bool getParsedParamDetails, bool getEscapedDelimiterOptions, out List<ParsedParameterDetails> parsedParams, out List<int> escapedDelimiterLocations)
        {
            parsedParams = getParsedParamDetails ? new List<ParsedParameterDetails>() : null;
            escapedDelimiterLocations = getEscapedDelimiterOptions ? new List<int>() : null;
            string error;
            if (string.IsNullOrEmpty(inputString))
            {
                return "Cannot be empty";
            }
            StringBuilder sb = new StringBuilder();
            bool isParsingParameter = false;
            int parameterBeginIndex = 0;
            for (int idx = 0; idx < inputString.Length; idx++)
            {
                if (!isParsingParameter)
                {
                    if (inputString[idx] == ParameterizationHelper.ParamDelimiterBegin)
                    {
                        isParsingParameter = true;
                        parameterBeginIndex = idx;
                        sb = new StringBuilder();
                    }
                }
                else // inside the parameter-ref syntax
                {
                    // Escaped ParamDelimiterBegin
                    if ((idx == parameterBeginIndex + 1) && (inputString[idx] == ParameterizationHelper.ParamDelimiterBegin))
                    {
                        if (escapedDelimiterLocations != null)
                        {
                            escapedDelimiterLocations.Add(parameterBeginIndex);
                        }
                        isParsingParameter = false;
                    }
                    else if (inputString[idx] != ParameterizationHelper.ParamDelimiterEnd)
                    {
                        sb.Append(inputString[idx]);
                    }
                    else
                    {
                        isParsingParameter = false;
                        string parameterRef = sb.ToString();
                        if (string.IsNullOrEmpty(parameterRef))
                        {
                            error = "Invalid parameter syntax. Cannot specify empty parameter declaration. Parameter name must be declared";
                            return error;
                        }
                        if (!parameterRef.StartsWith(ParameterizationHelper.ParameterSyntax, StringComparison.Ordinal))
                        {
                            error = "Invalid parameter syntax. Parameter must start with " + ParameterizationHelper.ParameterSyntax;
                            return error;
                        }
                        string paramName = parameterRef.Substring(ParameterizationHelper.ParameterSyntaxLength);
                        if (!ParameterizationHelper.ParameterRegEx.IsMatch(paramName))
                        {
                            error = "Invalid parameter syntax for parameter " + paramName;
                            return error;
                        }
                        string paramValue = null;
                        if (paramValues != null)
                        {
                            if (!paramValues.TryGetValue(paramName, out paramValue))
                            {
                                error = "Parameter value not specified for " + paramName;
                                return error;
                            }
                        }

                        if (parsedParams != null)
                        {
                            parsedParams.Add(new ParsedParameterDetails
                                {
                                    ParameterName = paramName,
                                    ParameterValue = paramValue,
                                    ParameterBeginIndex = parameterBeginIndex,
                                    ParameterLength = parameterRef.Length + 2  // for delimiters [ ]                                    
                                });
                        }
                    }
                }
            }
            if (isParsingParameter)
            {
                error = "Invalid parameter syntax. Parameter Delimiter was found missing";
                return error;
            }
            return null;
        }

        private static string ValidateInputNotParameterized(string inputString, ParameterOptions parameterOptions, bool isNotNullable = true)
        {
            string error;
            if (parameterOptions == ParameterOptions.CannotBeParameterizedBySpecification)
            {
                return ValidateCannotBeParameterizedBySpecification(inputString);
            }
            if (string.IsNullOrEmpty(inputString))
            {
                return isNotNullable ? "Cannot be empty" : null;
            }
            const string errorPrefix = "Incorrect parameterization. ";
            StringBuilder sb = new StringBuilder();
            bool isParsingParameter = false;
            int parameterBeginIndex = 0;
            for (int idx = 0; idx < inputString.Length; idx++)
            {
                if (!isParsingParameter)
                {
                    if (inputString[idx] == ParameterizationHelper.ParamDelimiterBegin)
                    {
                        isParsingParameter = true;
                        parameterBeginIndex = idx;
                        sb = new StringBuilder();
                    }
                }
                else // inside the parameter-ref syntax
                {
                    // Escaped ParamDelimiterBegin
                    if ((idx == parameterBeginIndex + 1) && (inputString[idx] == ParameterizationHelper.ParamDelimiterBegin))
                    {
                        isParsingParameter = false;
                    }
                    else if (inputString[idx] != ParameterizationHelper.ParamDelimiterEnd)
                    {
                        sb.Append(inputString[idx]);
                    }
                    else
                    {
                        string parameterRef = sb.ToString();
                        if (string.IsNullOrEmpty(parameterRef))
                        {
                            error = "Cannot specify empty parameter declaration";
                            return error;
                        }
                        if (!parameterRef.StartsWith(ParameterizationHelper.ParameterSyntax, StringComparison.Ordinal))
                        {
                            error = errorPrefix + "Does not start with Param syntax. If using '[' in a literal, must escape all occurancances of [ with another [";
                            return error;
                        }
                        string paramName = parameterRef.Substring(ParameterizationHelper.ParameterSyntaxLength);
                        if (ParameterizationHelper.ParameterRegEx.IsMatch(paramName))
                        {
                            error = errorPrefix + "(Parameter syntax is valid though.)";
                            return error;
                        }
                        if (paramName.Contains(" "))
                        {
                            error = errorPrefix + "Remove white spaces in param name.";
                            return error;
                        }
                        error = errorPrefix;
                        return error;
                    }
                }
            }
            if (isParsingParameter)
            {
                error = errorPrefix + "If using '[', escape all occurancances of [ with another [";
                return error;
            }
            return null;
        }


        public static string SubstituteParamConfigurationValuesInJson(string jsonDefinition, Dictionary<string, string> configuration)
        {
            string substitutedJsonDefinition = null;
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            Dictionary<string, object> jsonDict = (Dictionary<string, object>)serializer.DeserializeObject(jsonDefinition);
            if (jsonDict != null)
            {
                object jsonDomAsObj = jsonDict;
                ParameterizationHelper.SubstituteConfigurationValues(ref jsonDomAsObj, configuration);
                substitutedJsonDefinition = serializer.Serialize(jsonDomAsObj);
            }
            return substitutedJsonDefinition;
        }


        public static void SubstituteConfigurationValues(ref object element, Dictionary<string, string> configuration)
        {
            if (element == null)
            {
                return;
            }
            if (element.GetType() == typeof(Dictionary<string, object>))
            {
                Dictionary<string, object> dict = (Dictionary<string, object>)element;
                List<string> keys = dict.Keys.ToList();
                foreach (string key in keys)
                {
                    if (dict[key] is string)
                    {
                        dict[key] = ParameterizationHelper.SubstituteValues((string)dict[key], configuration);
                    }
                    else
                    {
                        object item = dict[key];
                        ParameterizationHelper.SubstituteConfigurationValues(ref item, configuration);
                        dict[key] = item;
                    }
                }
            }
            else if (element.GetType() == typeof(object[]))
            {
                List<object> substitutedItems = new List<object>();
                foreach (object item in (object[])element)
                {
                    if (item is string)
                    {
                        substitutedItems.Add(ParameterizationHelper.SubstituteValues((string)item, configuration));
                    }
                    else
                    {
                        object substituteItem = item;
                        ParameterizationHelper.SubstituteConfigurationValues(ref substituteItem, configuration);
                        substitutedItems.Add(substituteItem);
                    }
                    element = substitutedItems.ToArray();
                }
            }
        }


        /// <summary>
        /// Throws ApplicationException on error
        /// </summary>
        /// <param name="inputString"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static string SubstituteValues(string inputString, Dictionary<string, string> configuration)
        {
            if (string.IsNullOrEmpty(inputString))
            {
                return inputString;
            }
            StringBuilder sb = new StringBuilder(inputString);
            List<ParsedParameterDetails> parsedParams;
            List<int> escapedDelimiterLocations;
            string error = ParseForParametersAndValidateSyntax(inputString, configuration, true, true, out parsedParams, out escapedDelimiterLocations);
            if (error == null)
            {
                int indexOffset = 0; // the indexOffset to keep track of the changes in the offset due to substitution
                foreach (ParsedParameterDetails paramOutput in parsedParams)
                {
                    int originalRefLength = paramOutput.ParameterLength; // account for syntax delimiter chars
                    sb.Remove((paramOutput.ParameterBeginIndex + indexOffset), originalRefLength);
                    sb.Insert((paramOutput.ParameterBeginIndex + indexOffset), paramOutput.ParameterValue);

                    // Fix the offset due to this substitution
                    indexOffset += (paramOutput.ParameterValue.Length - originalRefLength);
                }
                // Remove the escaping for the delimeters
                foreach (int escapedDelimiterLocation in escapedDelimiterLocations)
                {
                    sb.Remove(escapedDelimiterLocation, 1);
                }
            }
            else
            {
                throw new ApplicationException(error);
            }
            return sb.ToString();
        }


        public static Dictionary<string, long> GetParameterUsageCount(ResourceDefinition resourceDefinition)
        {
            JavaScriptSerializer ser = new JavaScriptSerializer();
            string jsonInput = ser.Serialize(resourceDefinition);
            return GetParameterUsageCount(jsonInput);
        }

        public static Dictionary<string, long> GetParameterUsageCount(VMRoleResourceExtensionDefinition resourceExtensionDefinition)
        {
            JavaScriptSerializer ser = new JavaScriptSerializer();
            string jsonInput = ser.Serialize(resourceExtensionDefinition);
            return GetParameterUsageCount(jsonInput);
        }

        public static Dictionary<string, long> GetParameterUsageCount(string jsonInput)
        {
            Dictionary<string, long> parameterCount = new Dictionary<string, long>();
            JavaScriptSerializer ser = new JavaScriptSerializer();
            Dictionary<string, object> objectJsonDom = ser.DeserializeObject(jsonInput) as Dictionary<string, object>;
            if (objectJsonDom != null)
            {
                foreach (KeyValuePair<string, object> keyValuePair in objectJsonDom)
                {
                    GetParameterCount(keyValuePair.Value, parameterCount);
                }
            }
            return parameterCount;
        }

        private static void GetParameterCount(object element, Dictionary<string, long> parameterCount)
        {
            if (element == null)
            {
                return;
            }
            if (element.GetType() == typeof(Dictionary<string, object>))
            {
                Dictionary<string, object> dict = (Dictionary<string, object>)element;
                foreach (KeyValuePair<string, object> keyValuePair in dict)
                {
                    GetParameterCount(keyValuePair.Value, parameterCount);
                }
            }
            else if (element.GetType() == typeof(object[]))
            {
                foreach (object item in (object[])element)
                {
                    GetParameterCount(item, parameterCount);
                }
            }
            else
            {
                string val = element as string;
                if (val != null)
                {
                    List<string> paramNames;
                    string error = ValidateAndGetParameterNames(val, out paramNames);
                    if (string.IsNullOrEmpty(error))
                    {
                        foreach (string paramName in paramNames)
                        {
                            if (parameterCount.ContainsKey(paramName))
                            {
                                parameterCount[paramName]++;
                            }
                            else
                            {
                                parameterCount[paramName] = 1;
                            }
                        }
                    }
                }
            }
        }

    }
}
